﻿using System;
using System.Collections.Generic;
using System.Linq;
using Geo.Geodesy;
using SSG.Tools;

namespace SSG.Model
{
    /// <summary>
    /// Utilities for fixes, fixpoint lists
    /// </summary>
    public static class FixUtils
    {
        /// <summary>
        /// Find first fixpoint with altitude
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="startIndex"></param>
        /// <returns></returns>
        public static int FirstFixWithAltitudeIndex(this List<Fix> fixes, int startIndex = 0)
        {
            if (fixes.NullSafeIsEmpty()) return -1;
            for (int f = startIndex; f < fixes.Count; f++) {
                if (fixes[f].Altitude != null) return f;
            }
            return -1;
        }

        /// <summary>
        /// Maximum altitude
        /// </summary>
        /// <param name="fixes"></param>
        /// <returns></returns>
        public static Altitude MaxAltitude(this List<Fix> fixes)
        {
            if (fixes.NullSafeIsEmpty()) return null;
            List<Fix> fwa = fixes.Where(fx => fx.Altitude != null).ToList();
            Altitude a = null;
            foreach (Fix fix in fwa) {
                if (a == null)
                    a = fix.Altitude;
                else if (fix.Altitude.ValueFt > a.ValueFt)
                    a = fix.Altitude;
            }
            return a;
        }

        /// <summary>
        /// Maximum altitude
        /// </summary>
        /// <param name="fixes"></param>
        /// <returns></returns>
        public static Altitude MaxAltitudeRestriction(this List<Fix> fixes)
        {
            if (fixes.NullSafeIsEmpty()) return null;
            List<Fix> fwa = fixes.Where(fx => fx.AltitudeRestriction != null).ToList();
            Altitude a = null;
            foreach (Fix fix in fwa)
            {
                if (a == null)
                    a = fix.AltitudeRestriction;
                else if (fix.AltitudeRestriction.ValueFt > a.ValueFt)
                    a = fix.AltitudeRestriction;
            }
            return a;
        }
        
        /// <summary>
        /// Minimum altitude
        /// </summary>
        /// <param name="fixes"></param>
        /// <returns></returns>
        public static Altitude MinAltitude(this List<Fix> fixes)
        {
            if (fixes.NullSafeIsEmpty()) return null;
            List<Fix> fwa = fixes.Where(fx => fx.Altitude != null).ToList();
            Altitude a = null;
            foreach (Fix fix in fwa) {
                if (a == null)
                    a = fix.Altitude;
                else if (fix.Altitude.ValueFt < a.ValueFt)
                    a = fix.Altitude;
            }
            return a;
        }

        /// <summary>
        /// Minimum altitude
        /// </summary>
        /// <param name="fixes"></param>
        /// <returns></returns>
        public static Altitude MinAltitudeRestriction(this List<Fix> fixes)
        {
            if (fixes.NullSafeIsEmpty()) return null;
            List<Fix> fwa = fixes.Where(fx => fx.AltitudeRestriction != null).ToList();
            Altitude a = null;
            foreach (Fix fix in fwa)
            {
                if (a == null)
                    a = fix.AltitudeRestriction;
                else if (fix.AltitudeRestriction.ValueFt < a.ValueFt)
                    a = fix.AltitudeRestriction;
            }
            return a;
        }
        
        /// <summary>
        /// Runway among fixes
        /// </summary>
        /// <param name="fixes"></param>
        /// <returns></returns>
        public static Runway FindRunway(this List<Fix> fixes)
        {
            if (fixes.NullSafeIsEmpty()) return null;
            return fixes.FirstOrDefault(f => f is Runway) as Runway;
        }

        /// <summary>
        /// Altitude of runway
        /// </summary>
        /// <param name="fixes"></param>
        /// <returns></returns>
        public static Altitude FindRunwayAltitude(this List<Fix> fixes)
        {
            if (fixes.NullSafeIsEmpty()) return null;
            Runway rw = fixes.FindRunway();
            return rw == null ? null : rw.Altitude;
        }

        /// <summary>
        /// Fixes with altitude
        /// </summary>
        public static List<Fix> FixesWithAltitude(this List<Fix> fixes)
        {
            return fixes.NullSafeIsEmpty() ? new List<Fix>() : fixes.Where(fx => fx.Altitude != null).ToList();
        }

        /// <summary>
        /// Any fixes with altitude
        /// </summary>
        public static bool HasFixesWithAltitude(this List<Fix> fixes)
        {
            return !fixes.NullSafeIsEmpty() && fixes.Any(fix => fix.Altitude != null);
        }

        /// <summary>
        /// Insert a fixpoint
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="fixpoint"></param>
        /// <param name="clone"></param>
        public static void InsertFix(this List<Fix> fixes, Fix fixpoint, bool clone = true)
        {
            if (fixes == null) throw new ArgumentNullException("fixes");
            if (fixpoint == null) throw new ArgumentNullException("fixpoint");
            Fix f = clone ? fixpoint.Clone() as Fix : fixpoint;
            fixes.Insert(0, f);
        }

        /// <summary>
        /// Append a fixpoint
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="fixpoint"></param>
        /// <param name="clone"></param>
        public static void AddFix(this List<Fix> fixes, Fix fixpoint, bool clone = true)
        {
            if (fixes == null) throw new ArgumentNullException("fixes");
            if (fixpoint == null) throw new ArgumentNullException("fixpoint");
            Fix f = clone ? fixpoint.Clone() as Fix : fixpoint;
            fixes.Add(f);
        }

        /// <summary>
        /// Add fixes
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="toBeAdded"></param>
        /// <param name="clone"></param>
        public static void AddFixes(this List<Fix> fixes, List<Fix> toBeAdded, bool clone = true)
        {
            if (fixes == null) throw new ArgumentNullException("fixes");
            if (toBeAdded.NullSafeIsEmpty()) return;
            foreach (Fix fix in toBeAdded) {
                fixes.AddFix(fix, clone);
            }
        }

        /// <summary>
        /// Insert fixes
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="toBeInserted"></param>
        /// <param name="clone"></param>
        public static void InsertFixes(this List<Fix> fixes, List<Fix> toBeInserted, bool clone = true)
        {
            if (fixes == null) throw new ArgumentNullException("fixes");
            if (toBeInserted.NullSafeIsEmpty()) return;
            for (int f = (toBeInserted.Count - 1); f >= 0; f--) {
                Fix fixpoint = toBeInserted[f];
                fixes.InsertFix(fixpoint, clone);
            }
        }

        /// <summary>
        /// Distance along several fixes
        /// </summary>
        /// <param name="fixes"></param>
        /// <returns></returns>
        public static double DistanceKm(this List<Fix> fixes)
        {
            if (fixes.NullSafeCount() < 2) return 0.0;
            double d = 0.0;
            for (int f = 1; f < fixes.Count; f++) {
                d += fixes[f].ShortestDistanceKm(fixes[f - 1]);
            }
            return d;
        }

        /// <summary>
        /// Calculate legs
        /// </summary>
        public static List<Leg> CalculateLegs(this List<Fix> fixes)
        {
            List<Leg> legs = new List<Leg>();
            if (fixes.NullSafeCount() < 2) return legs;
            for (int fi = 1; fi < fixes.Count; fi++) {
                Fix fold = fixes[fi - 1];
                Fix fnew = fixes[fi];
                if (fold == null) throw new InvalidOperationException("Detected empty fix at " + (fi - 1));
                if (fnew == null) throw new InvalidOperationException("Detected empty fix at " + fi);
                if (fold.SamePosition(fnew)) continue;
                GeodeticLine line = fold.CalculateShortestLine(fnew);
                if (line == null) throw new InvalidOperationException("No line for " + fold + " -> " + fnew);
                Leg l = new Leg(fold, fnew, line.Distance.SiValue/1000.0, line.Bearing12);
                legs.Add(l);
            }
            return legs;
        }

        /// <summary>
        /// Given fixpoint last with an altitude?
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="fixpoint"></param>
        /// <returns></returns>
        public static bool IsLastFixWithAltitudeInList(this List<Fix> fixes, Fix fixpoint)
        {
            if (fixpoint == null || fixes.NullSafeIsEmpty()) return false;
            if (fixpoint.Altitude == null) return false;
            int i = fixes.IndexOf(fixpoint);
            if (i < 0) return false;
            for (int f = (i + 1); i < fixes.Count; f++) {
                if (fixes[f].Altitude != null) return false;
            }
            return true;
        }

        /// <summary>
        /// First fixpoint with altitude?
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="fixpoint"></param>
        /// <returns></returns>
        public static bool IsFirstFixWithAltitudeInList(this List<Fix> fixes, Fix fixpoint)
        {
            if (fixpoint == null || fixes.NullSafeIsEmpty()) return false;
            if (fixpoint.Altitude == null) return false;
            int i = fixes.IndexOf(fixpoint);
            if (i < 0) return false;
            for (int f = (i - 1); i >= 0; f--) {
                if (fixes[f].Altitude != null) return false;
            }
            return true;
        }

        /// <summary>
        /// Set altitude
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="alt"></param>
        /// <param name="clone"></param>
        public static void SetAltitudeCalculated(this List<Fix> fixes, Altitude alt, bool clone = true)
        {
            if (fixes.NullSafeIsEmpty()) return;
            foreach (Fix fix in fixes) {
                Altitude a = (clone && alt != null) ? alt.Clone() as Altitude : alt;
                if (a == null) throw new InvalidOperationException("Cloning altitude failed: " + alt);
                a.IsCalculated = true;
                fix.AltitudeCalculated = a;
            }
        }

        /// <summary>
        /// Calculate a climb / descent for given fixes
        /// </summary>
        /// <param name="fixes"></param>
        /// <param name="referenceFix"></param>
        /// <param name="rateFtPerKm"></param>
        /// <param name="maxValueFt"></param>
        public static void CalculateClimbOrDescent(this List<Fix> fixes, Fix referenceFix, double rateFtPerKm, double maxValueFt = -1)
        {
            if (referenceFix == null) throw new ArgumentNullException("referenceFix");
            if (fixes.NullSafeIsEmpty()) return;
            double distance = 0.0;
            double refAltitude = referenceFix.Altitude != null ? referenceFix.Altitude.ValueFt : 0;
            Fix lastFix = referenceFix;
            foreach (Fix fix in fixes) {
                double currentDistance = fix.ShortestDistanceKm(lastFix);
                distance += currentDistance;
                Altitude calcAltitude = new Altitude(refAltitude + distance*rateFtPerKm, Altitude.AltitudeMode.Msl, Altitude.AltitudeModifier.Exact, true);
                if (maxValueFt > 0 && calcAltitude.ValueFt > maxValueFt) calcAltitude.ValueFt = maxValueFt;
                fix.AltitudeCalculated = calcAltitude;
                lastFix = fix;
            }
        }
    }
}